package com.google.android.location.content.mymaps;

import static com.google.android.location.content.Constants.TAG;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.util.Log;

import com.google.android.locatiom.maps.myroutes.R;
import com.google.android.location.content.Constants;
import com.google.android.location.content.RouteTrackPointDetails;
import com.google.android.location.content.libcontent.RouteTrackPoint;
import com.google.android.location.content.maps.colors.RoutePathUtilities;
import com.google.android.location.content.maps.predict.colors.RoutePredictPathPainter;
import com.google.android.location.content.maps.predict.colors.RoutePredictPathPainterFactory;
import com.google.android.location.content.utilities.LocationFilter;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
import com.google.android.maps.Projection;



public class MyMapPredictionOverlay  extends Overlay implements OnSharedPreferenceChangeListener {

	  private final Drawable[] arrows;
	  private final int arrowWidth, arrowHeight;
	  private final Drawable statsMarker;
	  private final Drawable rtpMarker;
	  private final Drawable startMarker;
	  private final Drawable endMarker;
	  private final int markerWidth, markerHeight;
	  private final Paint errorCirclePaint;
	  private final Context context;
	  private final List<RouteTrackPoint> route_track_points;
	  private final List<CachedPredictLocation> points;
	  private final BlockingQueue<CachedPredictLocation> pendingPoints;

	  private boolean trackDrawingEnabled;
	  private int lastHeading = 0;
	  private Location myLocation;
	  private boolean showEndMarker = true;
	  private boolean alwaysVisible = true;

	  private GeoPoint lastReferencePoint;
	  private Rect lastViewRect;
	  private boolean lastPathExists;
	  private RoutePredictPathPainter routePredictPathPainter;

	  /**
	   * Represents a pre-processed {@code Location} to speed up drawing.
	   * This class is more like a data object and doesn't provide accessors.
	   */
	  public static class CachedPredictLocation {
	    public final boolean valid;
	    public final GeoPoint geoPoint;
	    public final int speed;
	    
	    /**
	     * Constructor for an invalid cached location.
	     */
	    public CachedPredictLocation() {
	      this.valid = false;
	      this.geoPoint = null;
	      this.speed = -1;
	    }

	    /**
	     * Constructor for a potentially valid cached location.
	     */
	    public CachedPredictLocation(Location location) {
	      this.valid = LocationFilter.isValidLocation(location);
	      this.geoPoint = valid ? LocationFilter.getGeoPoint(location) : null;
	      this.speed = (int) Math.floor(location.getSpeed() * 3.6);
	    }
	  };

	  public MyMapPredictionOverlay(Context context) {
	    this.context = context;
      //trackDrawingEnabled = true;
	    this.route_track_points = new ArrayList<RouteTrackPoint>();
	    this.points = new ArrayList<CachedPredictLocation>(1024);
	    this.pendingPoints = new ArrayBlockingQueue<CachedPredictLocation>(
	        Constants.MAX_DISPLAYED_ROUTE_POINTS, true);
	    
	    final Resources resources = context.getResources();
	    arrows = new Drawable[] {
	        resources.getDrawable(R.drawable.arrowp_0),
	        resources.getDrawable(R.drawable.arrowp_20),
	        resources.getDrawable(R.drawable.arrowp_40),
	        resources.getDrawable(R.drawable.arrowp_60),
	        resources.getDrawable(R.drawable.arrowp_80),
	        resources.getDrawable(R.drawable.arrowp_100),
	        resources.getDrawable(R.drawable.arrowp_120),
	        resources.getDrawable(R.drawable.arrowp_140),
	        resources.getDrawable(R.drawable.arrowp_160),
	        resources.getDrawable(R.drawable.arrowp_180),
	        resources.getDrawable(R.drawable.arrowp_200),
	        resources.getDrawable(R.drawable.arrowp_220),
	        resources.getDrawable(R.drawable.arrowp_240),
	        resources.getDrawable(R.drawable.arrowp_260),
	        resources.getDrawable(R.drawable.arrowp_280),
	        resources.getDrawable(R.drawable.arrowp_300),
	        resources.getDrawable(R.drawable.arrowp_320),
	        resources.getDrawable(R.drawable.arrowp_340)
	    };
	    arrowWidth = arrows[lastHeading].getIntrinsicWidth();
	    arrowHeight = arrows[lastHeading].getIntrinsicHeight();
	    for (Drawable arrow : arrows) 
	    {
	      arrow.setBounds(0, 0, arrowWidth, arrowHeight);
	    }

	    statsMarker = resources.getDrawable(R.drawable.yellowp_marker);
	    markerWidth = statsMarker.getIntrinsicWidth();
	    markerHeight = statsMarker.getIntrinsicHeight();
	    statsMarker.setBounds(0, 0, markerWidth, markerHeight);

	    startMarker = resources.getDrawable(R.drawable.greenp_dot);
	    startMarker.setBounds(0, 0, markerWidth, markerHeight);

	    endMarker = resources.getDrawable(R.drawable.redp_dot);
	    endMarker.setBounds(0, 0, markerWidth, markerHeight);

	    rtpMarker = resources.getDrawable(R.drawable.bluep_marker);
	    rtpMarker.setBounds(0, 0, markerWidth, markerHeight);

	    errorCirclePaint = RoutePathUtilities.getPaint(R.color.blue, context);
	    errorCirclePaint.setAlpha(127);
	    
	    routePredictPathPainter = RoutePredictPathPainterFactory.getRoutePredictPathPainter(context);
	    
	    context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
	        .registerOnSharedPreferenceChangeListener(this);
	  }

	  /**
	   * Add a location to the map overlay.
	   *
	   * NOTE: This method doesn't take ownership of the given location, so it is
	   * safe to reuse the same location while calling this method.
	   *
	   * @param l the location to add.
	   */
	  public void addLocation(Location l) 
	  {
	    // Queue up in the pending queue until it's merged with {@code #points}.
	    if (!pendingPoints.offer(new CachedPredictLocation(l))) 
	    {
	      Log.e(TAG, "Unable to add pending points");
	    }
	  }

	  /**
	   * Adds a segment split to the map overlay.
	   */  
	  
	  public void addSegmentSplit() 
	  {
	    if (!pendingPoints.offer(new CachedPredictLocation())) 
	    {
	      Log.e(TAG, "Unable to add pending points");
	    }
	  }

	  public void addRouteTrackPoint(RouteTrackPoint rtp) 
	  {
	    // Note: We don't cache route_track_points, because it's not worth the effort.
	    if (rtp != null && rtp.getLocation() != null) 
	    {
	      synchronized (route_track_points) 
	      {
	        route_track_points.add(rtp);
	      }
	    }
	  }

	  public int getNumLocations() 
	  {
	    synchronized (points) 
	    {
	      return points.size() + pendingPoints.size();
	    }
	  }

	  
	  public int getNumRouteTrackPoints() 
	  {
	    synchronized (route_track_points) 
	    {
	      return route_track_points.size();
	    }
	  }

	  public void clearPoints() 
	  {
	    synchronized (getPoints()) 
	    {
	      getPoints().clear();
	      pendingPoints.clear();
	      lastPathExists = false;
	      lastViewRect = null;
	      routePredictPathPainter.clear();
	    }
	  }

	  public void clearRouteTrackPoints() {
	    synchronized (route_track_points) {
	      route_track_points.clear();
	    }
	  }

	  public void setTrackDrawingEnabled(boolean trackDrawingEnabled) {
	    this.trackDrawingEnabled = trackDrawingEnabled;
	  }

	  public void setShowEndMarker(boolean showEndMarker) {
	    this.showEndMarker = showEndMarker;
	  }

	  @Override
	  public void draw(Canvas canvas, MapView mapView, boolean shadow) 
	  {
	    if (shadow) 
	    {
	      return;
	    }

	    // It's safe to keep projection within a single draw operation.
	    final Projection projection = getMapProjection(mapView);
	    if (projection == null) 
	    {
	      Log.w(TAG, "No projection, unable to draw");
	      return;
	    }

	    // Get the current viewing window.
	    if (trackDrawingEnabled) 
	    {
	      Rect viewRect = getMapViewRect(mapView);

	      // Draw the selected route:
	      drawRoute(canvas, projection, viewRect);

	      // Draw the "Start" and "End" markers:
	      drawMarkers(canvas, projection);
	      
	      // Draw the route_track_points:
	      drawRouteTrackPoints(canvas, projection);
	    }

	    // Draw the current location
	    drawMyLocation(canvas, projection);
	  }

	  private void drawMarkers(Canvas canvas, Projection projection) {
	    // Draw the "End" marker.
	    if (showEndMarker) 
	    {
	      for (int i = getPoints().size() - 1; i >= 0; --i) 
	      {
	        if (getPoints().get(i).valid) 
	        {
	          drawElement(canvas, projection, getPoints().get(i).geoPoint, endMarker, -markerWidth / 2, -markerHeight);
	          break;
	        }
	      }
	    }
	    
	    // Draw the "Start" marker.
	    for (int i = 0; i < getPoints().size(); ++i) 
	    {
	      if (getPoints().get(i).valid) 
	      {
	        drawElement(canvas, projection, getPoints().get(i).geoPoint, startMarker, -markerWidth / 2, -markerHeight);
	        break;
	      }
	    }
	  }
	
	  Projection getMapProjection(MapView mapView) 
	  {
	    return mapView.getProjection();
	  }

	 
	  Rect getMapViewRect(MapView mapView) 
	  {
	    int w = mapView.getLongitudeSpan();
	    int h = mapView.getLatitudeSpan();
	    int cx = mapView.getMapCenter().getLongitudeE6();
	    int cy = mapView.getMapCenter().getLatitudeE6();
	    return new Rect(cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2);
	  }
	 
	

	  private void drawRouteTrackPoints(Canvas canvas, Projection projection) 
	  {
	    synchronized (route_track_points) 
	    {
	      for (RouteTrackPoint rtp : route_track_points) 
	      {
	        Location loc = rtp.getLocation();
	        drawElement(canvas, projection, LocationFilter.getGeoPoint(loc),
	        		rtp.getType() == RouteTrackPoint.TRACKPOINT_STATISTICS ? statsMarker
	                : rtpMarker, -(markerWidth / 2) + 3, -markerHeight);
	      }
	    }
	  }

	  private void drawMyLocation(Canvas canvas, Projection projection) 
	  {
	    // Draw the arrow icon.
	    if (myLocation == null) 
	    {
	      return;
	    }

	    Point pt = drawElement(canvas, projection,
	        LocationFilter.getGeoPoint(myLocation), arrows[lastHeading], -(arrowWidth / 2) + 3, -(arrowHeight / 2));
	    // Draw the error circle.
	    float radius = projection.metersToEquatorPixels(myLocation.getAccuracy());
	    canvas.drawCircle(pt.x, pt.y, radius, errorCirclePaint);
	  }

	  private void drawRoute(Canvas canvas, Projection projection, Rect viewRect)
	  {
	    boolean draw;

	    synchronized (points) 
	    {
	      // Merge the pending points with the list of cached locations.
	      final GeoPoint referencePoint = projection.fromPixels(0, 0);
	      int newPoints = pendingPoints.drainTo(points); 
	      boolean newProjection = !viewRect.equals(lastViewRect) || !referencePoint.equals(lastReferencePoint); 
	      if (newPoints == 0 && lastPathExists && !newProjection) 
	      {
	        // No need to recreate path (same points and viewing area).
	        draw = true;
	      } 
	      else 
	      {
	        int numPoints = points.size();
	        if (numPoints < 2) 
	        {
	          // Not enough points to draw a path.
	          draw = false;
	        } 
	        else if (!routePredictPathPainter.needsRedraw() && lastPathExists && !newProjection) 
	        {
	          // Incremental update of the path, without repositioning the view.
	          draw = true;
	          routePredictPathPainter.updatePredictPath(projection, viewRect, numPoints - newPoints, alwaysVisible, points);  
	        } 
	        else 
	        {
	          // The view has changed so we have to start from scratch.
	          draw = true;
	          routePredictPathPainter.updatePredictPath(projection, viewRect, 0, alwaysVisible, points);  
	        }
	      }
	      lastReferencePoint = referencePoint;
	      lastViewRect = viewRect;
	    }
	    if (draw)
	    {
	      routePredictPathPainter.drawRoute(canvas);
	    }
	  }

	  // Visible for testing.
	  Point drawElement(Canvas canvas, Projection projection, GeoPoint geoPoint, Drawable element, int offsetX, int offsetY) 
	  {
	    Point pt = new Point();
	    projection.toPixels(geoPoint, pt);
	    canvas.save();
	    canvas.translate(pt.x + offsetX, pt.y + offsetY);
	    element.draw(canvas);
	    canvas.restore();
	    return pt;
	  }

	  /**
	   * Sets the pointer location (will be drawn on next invalidate).
	   */
	  public void setMyLocation(Location myLocation) 
	  {
	    this.myLocation = myLocation;
	  }

	  /**
	   * Sets the pointer heading in degrees (will be drawn on next invalidate).
	   *
	   * @return true if the visible heading changed (i.e. a redraw of pointer is
	   *         potentially necessary)
	   */
	  public boolean setHeading(float heading) 
	  {
	    int newhdg = Math.round(-heading / 360 * 18 + 180);
	    while (newhdg < 0)
	      newhdg += 18;
	    while (newhdg > 17)
	      newhdg -= 18;
	    if (newhdg != lastHeading) 
	    {
	      lastHeading = newhdg;
	      return true;
	    } 
	    else 
	    {
	      return false;
	    }
	  }

	  @Override
	  public boolean onTap(GeoPoint p, MapView mapView) 
	  {
	    if (p.equals(mapView.getMapCenter())) 
	    {
	      // There is (unfortunately) no good way to determine whether the tap was
	      // caused by an actual tap on the screen or the track ball. If the
	      // location is equal to the map center,then it was a track ball press with
	      // very high likelihood.
	      return false;
	    }

	    final Location tapLocation = LocationFilter.getLocation(p);
	    double dmin = Double.MAX_VALUE;
	    RouteTrackPoint routetrackpoint = null;
	    synchronized (route_track_points) 
	    {
	      for (int i = 0; i < route_track_points.size(); i++) 
	      {
	        final Location waypointLocation = route_track_points.get(i).getLocation();
	        if (waypointLocation == null) 
	        {
	          continue;
	        }
	        final double d = waypointLocation.distanceTo(tapLocation);
	        if (d < dmin) 
	        {
	          dmin = d;
	          routetrackpoint = route_track_points.get(i);
	        }
	      }
	    }

	    if (routetrackpoint != null &&
	        dmin < 15000000 / Math.pow(2, mapView.getZoomLevel())) {
	      Intent intent = new Intent(context, RouteTrackPointDetails.class);
	      intent.putExtra(RouteTrackPointDetails.ROUTE_TRACK_POINT_ID_EXTRA, routetrackpoint.getId());
	      context.startActivity(intent);
	      return true;
	    }
	    return super.onTap(p, mapView);
	  }
	  
	  /**
	   * @return the points
	   */
	  public List<CachedPredictLocation> getPoints() 
	  {
	    return points;
	  }
	  
	  @Override
	  public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) 
	  {
	    Log.d(TAG, "MapOverlay: onSharedPreferences changed " + key);
	    if (key != null) 
	    {
	      if (key.equals(context.getString(R.string.route_color_mode_key))) 
	      {
	        routePredictPathPainter = RoutePredictPathPainterFactory.getRoutePredictPathPainter(context);
	      }
	    }
	  }
	 
	}